package ru.nsu.fit.iconveyor.conveyor.storage.importer.graphml;

import java.util.*;

import org.apache.commons.lang.StringEscapeUtils;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import ru.nsu.fit.iconveyor.conveyor.Conveyor;
import ru.nsu.fit.iconveyor.conveyor.Globals;
import ru.nsu.fit.iconveyor.conveyor.storage.Utils;
import ru.nsu.fit.iconveyor.conveyor.storage.importer.graphml.elements.GraphMLAttribute;
import ru.nsu.fit.iconveyor.conveyor.storage.importer.graphml.elements.GraphMLEdge;
import ru.nsu.fit.iconveyor.conveyor.storage.importer.graphml.elements.GraphMLGraph;
import ru.nsu.fit.iconveyor.conveyor.storage.importer.graphml.elements.GraphMLVertex;
import ru.nsu.fit.iconveyor.conveyor.node.Node;

/**
 * GraphML parser.
 * @author Timur Zolotuhin (tzolotuhin@gmail.com)
 */
class GraphMLParser extends DefaultHandler {
    // schema parsing
    private String m_key;
    private String m_id;
    private String m_for;
    private String m_name;
    private String m_type;
    private String m_dflt;

    //private String error;
    private StringBuffer m_sbuf = new StringBuffer();

    // node, edge, data parsing
    private boolean inSchema;
    private boolean inGraphSection = false;

    private HashMap<String, GraphMLAttribute> m_vertData = new HashMap<String, GraphMLAttribute>();
    private HashMap<String, GraphMLAttribute> m_edgeData = new HashMap<String, GraphMLAttribute>();

    private final Stack<GraphMLGraph> lastSubGraph = new Stack<GraphMLGraph>();

    // Main data
    private Map<String, Class<Node>> classes;

    // result
    private final Conveyor conveyor;

    GraphMLParser(Map<String, Class<Node>> classes) {
        this.conveyor = new Conveyor();
        this.classes = classes;
    }

    @Override
    public void startDocument() {
        inSchema = true;
    }

    @Override
    public void endDocument() throws SAXException {
        // if we don't reach the end of document, delete graph from model
    }

    @Override
    public void startElement(String namespaceURI, String localName, String qName, Attributes atts) {
        // first clear the character buffer.
        m_sbuf.setLength(0);

        // FIX ME. If we interchange 'if', we have get increase, 
        // because label 'GRAPH' less common than 'NODE' or 'EDGE'.
        // If you read it, make it. And write test which show it=)
        if (qName.equals(GraphMLTokens.GRAPH)) {
            // parse directedness default
            String edef = atts.getValue(GraphMLTokens.EDGEDEF);
            boolean directed = GraphMLTokens.DIRECTED.equalsIgnoreCase(edef);
            String id = atts.getValue(GraphMLTokens.ID);

            lastSubGraph.push(new GraphMLGraph(id, directed));
            inGraphSection = true;
        } else if (qName.equals(GraphMLTokens.KEY)) {
            if (!inSchema) {
                error("\"" + GraphMLTokens.KEY + "\" elements can not" + " occur after the first node or edge declaration.");
            }
            m_for = atts.getValue(GraphMLTokens.FOR);
            m_id = atts.getValue(GraphMLTokens.ID);
            m_name = atts.getValue(GraphMLTokens.ATTRNAME);
            m_type = atts.getValue(GraphMLTokens.ATTRTYPE);
        } else if (qName.equals(GraphMLTokens.NODE)) {
            inSchema = false;
            inGraphSection = false;

            String nodeId = atts.getValue(GraphMLTokens.ID);
            GraphMLVertex v = lastSubGraph.peek().addVertex(nodeId);
            for (GraphMLAttribute attr : m_vertData.values()) {
                if (attr.getValue() != null) {
                    v.getAttributes().add(new GraphMLAttribute(attr.getName(), attr.getType(), attr.getValue()));
                }
            }
        } else if (qName.equals(GraphMLTokens.EDGE)) {
            inSchema = false;
            inGraphSection = false;

            String edgeId = atts.getValue(GraphMLTokens.ID);
            String srcId = atts.getValue(GraphMLTokens.SOURCE);
            String trgId = atts.getValue(GraphMLTokens.TARGET);

            GraphMLEdge e = lastSubGraph.peek().addEdge(edgeId, srcId, trgId);
            for (GraphMLAttribute attr : m_edgeData.values()) {
                if (attr.getValue() != null) {
                    e.getAttributes().add(new GraphMLAttribute(attr.getName(), attr.getType(), attr.getValue()));
                }
            }
        } else if (qName.equals(GraphMLTokens.DATA)) {
            m_key = atts.getValue(GraphMLTokens.KEY);
        }
    }

    @Override
    public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        if (qName.equals(GraphMLTokens.DEFAULT)) {
            // value is in the buffer
            m_dflt = m_sbuf.toString();
        } else if (qName.equals(GraphMLTokens.KEY)) {
            // time to add to the proper schema(s)
            addToSchema();
        } else if (qName.equals(GraphMLTokens.GRAPH)) {
            //NestedGraph e = lastGraph.pop();
            lastSubGraph.pop();
            inGraphSection = false;
        } else if (qName.equals(GraphMLTokens.NODE)) {
            inGraphSection = true;

            if (lastSubGraph.size() == 1) {
                GraphMLVertex v = lastSubGraph.peek().getVertices().pop();

                // search executor
                Node node = null;
                for (GraphMLAttribute buf : v.getAttributes()) {
                    if (buf.getName() != null && buf.getName().equals(Globals.EXECUTOR_ATTR_NAME)) {
                        Class<Node> clazz = classes.get(buf.getValue());
                        try {
                            node = clazz.getDeclaredConstructor(String.class).newInstance(v.getId());
                            conveyor.addNode(node);
                            break;
                        } catch (Exception ex) {
                            throw new SAXException("Bad instance of executor. Executor class = " + clazz);
                        }
                    }
                }

                // add parameters to node
                if (node != null) {
                    for (GraphMLAttribute buf : v.getAttributes()) {
                        if (buf.getName() != null && !buf.getName().equals(Globals.EXECUTOR_ATTR_NAME)) {
                            String parameterName = Utils.getAttributeNameForNodeFromKey(buf.getName(), node.getId());
                            node.setParameter(parameterName, buf.getValue());
                        }
                    }
                }
            }
        } else if (qName.equals(GraphMLTokens.EDGE)) {
            inGraphSection = true;

            GraphMLEdge e = lastSubGraph.peek().getEdges().pop();
            GraphMLAttribute inputAttr = null, outputAttr = null;
            for (GraphMLAttribute attr : e.getAttributes()) {
                if (attr.getName().equals(Globals.INPUT_ATTR_NAME)) {
                    inputAttr = attr;
                }
                if (attr.getName().equals(Globals.OUTPUT_ATTR_NAME)) {
                    outputAttr = attr;
                }
            }

            if (inputAttr != null && outputAttr != null) {
                Collection<Node> nodes = conveyor.getNodes();
                for (Node node : nodes) {
                    if (node.getId().equals(e.getTrgId())) {
                        node.getInputNodeConnectionByName(inputAttr.getValue().toString()).setChannelId(e.getId());
                    }
                    if (node.getId().equals(e.getSrcId())) {
                        node.getOutputNodeConnectionByName(outputAttr.getValue().toString()).setChannelId(e.getId());
                    }
                }
                conveyor.addChannelId(e.getId());
            } else {
                error("Channel with id = " + e.getId() + " hasn't INPUT or OUTPUT");
            }
        } else if (qName.equals(GraphMLTokens.DATA)) {
            // value is in the buffer
            String value = StringEscapeUtils.unescapeJava(m_sbuf.toString());

            if (inGraphSection) {
                // name of subgraph
            } else {

                if (m_vertData.get(m_key) != null) {
                    GraphMLAttribute a = m_vertData.get(m_key);
                    boolean newAttr = true;
                    for (GraphMLAttribute attr : lastSubGraph.peek().getVertices().peek().getAttributes()) {
                        if (attr.getName().equals(a.getName())) {
                            attr.setValue(value);
                            newAttr = false;
                        }
                    }
                    if (newAttr) {
                        lastSubGraph.peek().getVertices().peek().getAttributes().add(new GraphMLAttribute(a.getName(), a.getType(), value));
                    }
                } else if (m_edgeData.get(m_key) != null) {
                    GraphMLAttribute a = m_edgeData.get(m_key);
                    boolean newAttr = true;
                    for (GraphMLAttribute attr : lastSubGraph.peek().getEdges().peek().getAttributes()) {
                        if (attr.getName().equals(a.getName())) {
                            attr.setValue(value);
                            newAttr = false;
                        }
                    }
                    if (newAttr) {
                        lastSubGraph.peek().getEdges().peek().getAttributes().add(new GraphMLAttribute(a.getName(), a.getType(), value));
                    }
                } else {
                    error("found data tag in wrong position");
                }
            }
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        m_sbuf.append(ch, start, length);
    }

    public Conveyor getConveyor() {
        return conveyor;
    }

    //=============================================================================
//-----------------------PRIVATE METHODS---------------------------------------
    private void addToSchema() {
        if (m_name == null || m_name.length() == 0) {
            error("Empty " + GraphMLTokens.KEY + " name.");
        }
        if (m_type == null || m_type.length() == 0) {
            error("Empty " + GraphMLTokens.KEY + " type.");
        }

        //Class<?> type = DataBaseTypeParser.string2class(m_type);
        Class<?> type = String.class;

        Object local_default = m_dflt == null ? null : new String();
        if (m_for == null || m_for.equals(GraphMLTokens.ALL)) {
            m_vertData.put(m_id, new GraphMLAttribute(m_name, type, local_default));
            m_edgeData.put(m_id, new GraphMLAttribute(m_name, type, local_default));
        } else if (m_for.equals(GraphMLTokens.NODE)) {
            m_vertData.put(m_id, new GraphMLAttribute(m_name, type, local_default));
        } else if (m_for.equals(GraphMLTokens.EDGE)) {
            m_edgeData.put(m_id, new GraphMLAttribute(m_name, type, local_default));
        } else {
            error("Unrecognized \"" + GraphMLTokens.FOR + "\" value: " + m_for);
        }

        m_dflt = null;

    }

    private void error(String s) {
        throw new RuntimeException(s);
    }

} // end of inner class GraphMLHandler